a tool for shared writing and social publishing
1import { subscribeToPublication } from "app/lish/subscribeToPublication";
2import { cookies } from "next/headers";
3import { redirect } from "next/navigation";
4import { NextRequest, NextResponse } from "next/server";
5import { createOauthClient } from "src/atproto-oauth";
6import { setAuthToken } from "src/auth";
7
8import { supabaseServerClient } from "supabase/serverClient";
9import { URLSearchParams } from "url";
10import {
11 ActionAfterSignIn,
12 parseActionFromSearchParam,
13} from "./afterSignInActions";
14
15type OauthRequestClientState = {
16 redirect: string | null;
17 action: ActionAfterSignIn | null;
18};
19
20export async function GET(
21 req: NextRequest,
22 props: { params: Promise<{ route: string; handle?: string }> },
23) {
24 const params = await props.params;
25 let client = await createOauthClient();
26 switch (params.route) {
27 case "metadata":
28 return NextResponse.json(client.clientMetadata);
29 case "jwks":
30 return NextResponse.json(client.jwks);
31 case "login": {
32 const searchParams = req.nextUrl.searchParams;
33 const handle = searchParams.get("handle") as string;
34 // Put originating page here!
35 let redirect = searchParams.get("redirect_url");
36 if (redirect) redirect = decodeURIComponent(redirect);
37 let action = parseActionFromSearchParam(searchParams.get("action"));
38 let state: OauthRequestClientState = { redirect, action };
39
40 // Revoke any pending authentication requests if the connection is closed (optional)
41 const ac = new AbortController();
42
43 const url = await client.authorize(handle || "https://bsky.social", {
44 scope: "atproto transition:generic transition:email",
45 signal: ac.signal,
46 state: JSON.stringify(state),
47 });
48
49 return NextResponse.redirect(url);
50 }
51 case "callback": {
52 const params = new URLSearchParams(req.url.split("?")[1]);
53
54 let redirectPath = "/";
55 try {
56 const { session, state } = await client.callback(params);
57 let s: OauthRequestClientState = JSON.parse(state || "{}");
58 redirectPath = decodeURIComponent(s.redirect || "/");
59 let { data: identity } = await supabaseServerClient
60 .from("identities")
61 .select()
62 .eq("atp_did", session.did)
63 .single();
64 if (!identity) {
65 let existingIdentity = (await cookies()).get("auth_token");
66 if (existingIdentity) {
67 let data = await supabaseServerClient
68 .from("email_auth_tokens")
69 .select("*, identities(*)")
70 .eq("id", existingIdentity.value)
71 .single();
72 if (data.data?.identity && data.data.confirmed)
73 await supabaseServerClient
74 .from("identities")
75 .update({ atp_did: session.did })
76 .eq("id", data.data.identity);
77
78 return handleAction(s.action, redirectPath);
79 }
80 const { data } = await supabaseServerClient
81 .from("identities")
82 .insert({ atp_did: session.did })
83 .select()
84 .single();
85 identity = data;
86 }
87 let { data: token } = await supabaseServerClient
88 .from("email_auth_tokens")
89 .insert({
90 identity: identity!.id,
91 confirmed: true,
92 confirmation_code: "",
93 })
94 .select()
95 .single();
96
97 if (token) await setAuthToken(token.id);
98
99 // Process successful authentication here
100 console.log("authorize() was called with state:", state);
101
102 console.log("User authenticated as:", session.did);
103 return handleAction(s.action, redirectPath);
104 } catch (e) {
105 redirect(redirectPath);
106 }
107 }
108 default:
109 return NextResponse.json({ error: "Invalid route" }, { status: 404 });
110 }
111}
112
113const handleAction = async (
114 action: ActionAfterSignIn | null,
115 redirectPath: string,
116) => {
117 let parsePath = decodeURIComponent(redirectPath);
118 let url;
119 if (parsePath.includes("://")) url = new URL(parsePath);
120 else url = new URL(decodeURIComponent(redirectPath), "https://example.com");
121 if (action?.action === "subscribe") {
122 let result = await subscribeToPublication(action.publication);
123 if (result.success && result.hasFeed === false)
124 url.searchParams.set("showSubscribeSuccess", "true");
125 }
126
127 let path = url.pathname;
128 if (url.search) path += url.search;
129 if (url.hash) path += url.hash;
130 return parsePath.includes("://") ? redirect(url.toString()) : redirect(path);
131};